;;; sdcv-mode.el --- major mode to do dictionary query through sdcv

;; Copyright 2006 pluskid
;;
;; Author: pluskid@gmail.com
;; Version: $Id: sdcv-mode.el,v 0.0 2006/12/21 11:20:51 kid Exp $
;; Keywords: sdcv dictionary
;; X-URL: not distributed yet

;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, write to the Free Software
;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

;;; Commentary:

;; This is a major mode to view output of dictionary search of sdcv.

;; Put this file into your load-path and the following into your
;; ~/.emacs:
;;   (require 'sdcv-mode)
;;   (global-set-key (kbd "C-c d") 'sdcv-search)

;;; Code:

(require 'outline)
(provide 'sdcv-mode)
(eval-when-compile
  (require 'cl))

;;; ==================================================================
;;; Frontend, search word and display sdcv buffer
(defun sdcv-search (force-all-dictionaries)
  "Prompt for a word to search through sdcv.
When provided with a prefix argument, use all the dictionaries
no matter what `sdcv-dictionary-list' is."
  (interactive "P")
  (let ((word (if (and transient-mark-mode mark-active)
                  (buffer-substring-no-properties (region-beginning)
                                                  (region-end))
                (sdcv-current-word)))
        ;; note that elisp is dynamic-scoped
        (sdcv-dictionary-list (if force-all-dictionaries
                                  nil
                                sdcv-dictionary-list)))
    (setq word (read-string
                (format "Search the dictionary for (default %s): "
                        word)
                nil nil word))
    (sdcv-search-word word)))

(defun sdcv-search-word (word)
  "Search WORD through the command-line tool sdcv.
The result will be displayed in buffer named with
`sdcv-buffer-name' with `sdcv-mode'."
  (with-current-buffer (get-buffer-create sdcv-buffer-name)
    (setq buffer-read-only nil)
    (erase-buffer)
    (let ((process (start-process
                    "sdcv"
                    sdcv-buffer-name
                    "sdcv"
                    (sdcv-generate-dictionary-argument)
                    "-n"
                    (shell-quote-argument word))))
      (set-process-sentinel
       process
       (lambda (process signal)
         (when (memq (process-status process) '(exit signal))
           (unless (eq (current-buffer) (sdcv-get-buffer))
             (sdcv-goto-sdcv))
           (sdcv-mode-reinit)))))))
(defun sdcv-generate-dictionary-argument ()
  "Generate dictionary argument for sdcv from `sdcv-dictionary-list'."
  (if (null sdcv-dictionary-list)
      ""
    (mapconcat (lambda (dict)
                 (concat "-u "
                         (shell-quote-argument dict)))
               sdcv-dictionary-list
               " ")))

;;; ==================================================================
;;; utilities to switch from and to sdcv buffer
(defun sdcv-current-word ()
  "Get the current word under the cursor."
  (if (or (< emacs-major-version 21)
          (and (= emacs-major-version 21)
               (< emacs-minor-version 4)))
      (sdcv-current-word-1)
    ;; We have a powerful `current-word' function since 21.4
    (current-word nil t)))
(defun sdcv-current-word-1 ()
  (save-excursion
    (backward-word 1)
    (mark-word 1)
    (buffer-substring-no-properties (region-beginning)
                                    (region-end))))
(defvar sdcv-previous-window-conf nil
  "Window configuration before switching to sdcv buffer.")
(defun sdcv-goto-sdcv ()
  "Switch to sdcv buffer in other window."
  (interactive)
  (setq sdcv-previous-window-conf (current-window-configuration))
  (let* ((buffer (sdcv-get-buffer))
         (window (get-buffer-window buffer)))
    (if (null window)
        (switch-to-buffer-other-window buffer)
      (select-window window))))
(defun sdcv-return-from-sdcv ()
  "Bury sdcv buffer and restore the previous window configuration."
  (interactive)
  (if (window-configuration-p sdcv-previous-window-conf)
      (progn
        (set-window-configuration sdcv-previous-window-conf)
        (setq sdcv-previous-window-conf nil)
        (bury-buffer (sdcv-get-buffer)))
    (bury-buffer)))

(defun sdcv-get-buffer ()
  "Get the sdcv buffer. Create one if there's none."
  (let ((buffer (get-buffer-create sdcv-buffer-name)))
    (with-current-buffer buffer
      (unless (eq major-mode 'sdcv-mode)
        (sdcv-mode)))
    buffer))

;;; ==================================================================
;;; The very major mode
(defvar sdcv-mode-font-lock-keywords
  '(
    ;; dictionary name
    ("^-->\\(.*\\)$" . (1 font-lock-type-face))
    ;; property of word
    ("^<<\\([^>]*\\)>>$" . (1 font-lock-comment-face))
    ;; phonetic symbol
    ("^\\[\\([^]]*\\)\\]$" . (1 font-lock-string-face))
    )
  "Expressions to hilight in `sdcv-mode'")

(defvar sdcv-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map "q" 'sdcv-return-from-sdcv)
    (define-key map "s" 'isearch-forward-regexp)
    (define-key map "r" 'isearch-backward-regexp)
    (define-key map (kbd "C-s") 'isearch-forward)
    (define-key map (kbd "C-r") 'isearch-backward)
    (define-key map (kbd "RET") 'sdcv-mode-scroll-up-one-line)
    (define-key map (kbd "M-RET") 'sdcv-mode-scroll-down-one-line)
    (define-key map "v" 'scroll-up)
    (define-key map (kbd "M-v") 'scroll-down)
    (define-key map " " 'scroll-up)
    (define-key map (kbd "DEL") 'scroll-down)
    (define-key map (kbd "C-n") 'sdcv-mode-next-line)
    (define-key map "n" 'sdcv-mode-next-line)
    (define-key map (kbd "C-p") 'sdcv-mode-previous-line)
    (define-key map "p" 'sdcv-mode-previous-line)
    (define-key map "d" 'sdcv-search)
    (define-key map "?" 'describe-mode)
    (define-key map "a" 'show-all)
    (define-key map "h" 'hide-body)
    (define-key map "e" 'show-entry)
    (define-key map "c" 'hide-entry)
    map)
  "Keymap for `sdcv-mode'.")

(define-derived-mode sdcv-mode nil "sdcv"
  "Major mode to look up word through sdcv.
\\{sdcv-mode-map}
Turning on Text mode runs the normal hook `sdcv-mode-hook'."
  (setq font-lock-defaults '(sdcv-mode-font-lock-keywords))
  (setq buffer-read-only t)
  (set (make-local-variable 'outline-regexp) "^-->.*\n-->"))

(defun sdcv-mode-reinit ()
  "Re-initialize buffer.
Hide all entrys but the first one and goto
the beginning of the buffer."
  (ignore-errors
    (setq buffer-read-only t)
    (hide-body)
    (goto-char (point-min))
    (next-line 1)
    (show-entry)))

(defun sdcv-mode-scroll-up-one-line ()
  (interactive)
  (scroll-up 1))
(defun sdcv-mode-scroll-down-one-line ()
  (interactive)
  (scroll-down 1))
(defun sdcv-mode-next-line ()
  (interactive)
  (ignore-errors
    (next-line 1)
    (save-excursion
      (beginning-of-line nil)
      (when (looking-at outline-regexp)
        (show-entry)))))
;; I decide not to fold the definition entry when
;; doing previous-line. So `sdcv-mode-previous-line'
;; is only an alias of `previous-line'.
(defalias 'sdcv-mode-previous-line 'previous-line)

;;;;##################################################################
;;;;  User Options, Variables
;;;;##################################################################


(defvar sdcv-buffer-name "*sdcv*"
  "The name of the buffer of sdcv.")
(defvar sdcv-dictionary-list nil
  "A list of dictionaries to use.
Each entry is a string denoting the name of a dictionary, which
is then passed to sdcv through the '-u' command line option. If
this list is nil then all the dictionaries will be used.")

;;; sdcv-mode.el ends here
